在Three.js的3D场景中,倒影效果能够显著提升视觉真实感和场景的沉浸体验。本篇教程将深入讲解倒影实现的核心原理和技术细节。
先来看下效果:
什么是倒影?
倒影本质上是物体在反射平面(通常是地面)上的镜像投影。在3D图形学中,我们通过数学变换来模拟这种视觉效果。
基本思路
想象一下现实中的倒影:你站在湖边,水面上会出现你的倒影。在3D世界里,我们可以这样模拟:
- 复制一个一模一样的物体
- 把它上下翻转
- 放到"地面"的下方
- 让它半透明,看起来像倒影
核心知识解析
创建倒影物体
首先,我们需要复制原来的物体:
function createAdvancedReflection(originalMesh) {
const reflectionGeometry = originalMesh.geometry.clone();
const reflectionMaterial = createGradientReflectionMaterial(originalMesh.material);
const reflectionMesh = new THREE.Mesh(reflectionGeometry, reflectionMaterial);
// ...
}
这里用了clone()
来复制几何体和材质,这样倒影和原物体就不会互相影响了。
翻转物体
接下来就是关键的翻转操作:
reflectionMesh.scale.y = -1;
这行代码把物体在Y轴方向缩放了-1倍,相当于上下翻转。原来朝上的面现在朝下了,就像镜子里的效果一样。
摆放位置
翻转完了,还要把倒影放到正确的位置。对于立方体这种简单的情况:
reflectionMesh.position.y = -originalMesh.position.y;
如果原来的立方体在y=1.5的位置,倒影就放在y=-1.5,这样它们就关于地面(y=0)对称了。
让倒影更真实
单纯的翻转和移位还不够真实,我们需要让倒影有渐变透明的效果。现实中的倒影不是均匀的,通常是上面清晰,下面逐渐模糊消失。这需要渐变透明效果。
这就需要修改材质的着色器:
function createGradientReflectionMaterial(originalMaterial) {
const material = originalMaterial.clone();
material.onBeforeCompile = (shader) => {
// 在着色器中添加渐变效果
};
material.transparent = true;
material.side = THREE.DoubleSide;
return material;
}
onBeforeCompile
是Three.js提供的一个钩子,让我们可以在材质编译前插入自定义代码。我们利用它来实现渐变透明效果。
我们需要知道每个像素在世界空间中的Y坐标,然后根据高度计算透明度,越接近地面的部分越透明,越远离地面的部分越不透明。
// 在顶点着色器中计算世界位置
shader.vertexShader = shader.vertexShader.replace(
"#include <begin_vertex>",
`
#include <begin_vertex>
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
`
);
// 在片元着色器中基于Y坐标计算透明度
shader.fragmentShader = shader.fragmentShader.replace(
"gl_FragColor = vec4( outgoingLight, diffuseColor.a );",
`
float fadeDistance = 2.0;
float fade = 1.0 - clamp((vWorldPosition.y + fadeDistance) / fadeDistance, 0.0, 1.0);
gl_FragColor = vec4( outgoingLight, diffuseColor.a * fade * 0.6 );
`
);
渐变算法解析:
vWorldPosition.y + fadeDistance
:让计算基准上移(因为倒影在地面以下)/ fadeDistance
:归一化到0-1范围clamp(..., 0.0, 1.0)
:确保值不会超出有效范围1.0 -
:反转,让接近地面的部分更透明
如何让整个场景协调?
有了基础倒影,我们还需要考虑光照和阴影,让倒影与整个场景融为一体。
阴影系统
renderer.shadowMap.enabled = true; // 启用阴影
cube.castShadow = true; // 物体投射阴影
ground.receiveShadow = true; // 地面接收阴影
为什么需要阴影? 阴影为场景提供了深度和真实感,与倒影配合能创造更加convincing的视觉效果。
光照配置
const ambientLight = new THREE.AmbientLight(0x404040, 0.3); // 环境光提供基础照明
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8); // 方向光提供主要照明和阴影
翻转后的物体可能会有显示问题,因为原本朝外的面现在朝里了。所以我们设置:
material.side = THREE.DoubleSide;
完整代码
基于以上知识点,这里是完整的可运行代码:
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
// 场景基础设置
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
document.body.appendChild(renderer.domElement);
// 创建渐变倒影材质
function createGradientReflectionMaterial(originalMaterial) {
const material = originalMaterial.clone();
// 添加自定义着色器
material.onBeforeCompile = (shader) => {
// 添加顶点变量
shader.vertexShader =
"varying vec3 vWorldPosition;\n" + shader.vertexShader;
shader.vertexShader = shader.vertexShader.replace(
"#include <begin_vertex>",
`
#include <begin_vertex>
vWorldPosition = (modelMatrix * vec4(position, 1.0)).xyz;
`
);
// 添加片元变量和渐变计算
shader.fragmentShader =
"varying vec3 vWorldPosition;\n" + shader.fragmentShader;
shader.fragmentShader = shader.fragmentShader.replace(
"gl_FragColor = vec4( outgoingLight, diffuseColor.a );",
`
// 计算渐变透明度
float fadeDistance = 2.0;
float fade = 1.0 - clamp((vWorldPosition.y + fadeDistance) / fadeDistance, 0.0, 1.0);
gl_FragColor = vec4( outgoingLight, diffuseColor.a * fade * 0.6 );
`
);
};
material.transparent = true;
material.side = THREE.DoubleSide; // 双面渲染确保倒影可见
return material;
}
// 创建高级倒影函数
function createAdvancedReflection(originalMesh) {
const reflectionGeometry = originalMesh.geometry.clone();
const reflectionMaterial = createGradientReflectionMaterial(
originalMesh.material
);
const reflectionMesh = new THREE.Mesh(reflectionGeometry, reflectionMaterial);
reflectionMesh.scale.y = -1;
reflectionMesh.position.copy(originalMesh.position);
// 针对不同几何体的位置计算
if (originalMesh.geometry instanceof THREE.SphereGeometry) {
const radius = originalMesh.geometry.parameters.radius;
reflectionMesh.position.y = -(originalMesh.position.y - radius) - radius;
} else {
reflectionMesh.position.y = -originalMesh.position.y;
}
return reflectionMesh;
}
// 创建主要对象(立方体)
const cubeGeometry = new THREE.BoxGeometry(1, 1, 1);
const cubeMaterial = new THREE.MeshPhongMaterial({
color: 0xff4444,
shininess: 100,
});
const cube = new THREE.Mesh(cubeGeometry, cubeMaterial);
cube.position.y = 1.5; // 立方体底部距离地面1.0单位
cube.castShadow = true;
scene.add(cube);
// 创建倒影(使用高级渐变效果)
const reflectionCube = createAdvancedReflection(cube);
scene.add(reflectionCube);
// 添加反射地面
const groundGeometry = new THREE.PlaneGeometry(10, 10);
const groundMaterial = new THREE.MeshPhongMaterial({
color: 0x222222,
transparent: true,
opacity: 0.9,
shininess: 200,
});
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = 0;
ground.receiveShadow = true;
scene.add(ground);
// 照明设置
const ambientLight = new THREE.AmbientLight(0x404040, 0.3);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(5, 10, 5);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
scene.add(directionalLight);
// 摄像机位置
camera.position.set(3, 3, 5);
camera.lookAt(0, 0, 0);
// 添加轨道控制器
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
// 动画循环
function animate() {
requestAnimationFrame(animate);
// 旋转物体和倒影保持同步
cube.rotation.x += 0.01;
cube.rotation.y += 0.01;
reflectionCube.rotation.x += 0.01;
reflectionCube.rotation.y += 0.01;
controls.update();
renderer.render(scene, camera);
}
// 窗口大小调整
window.addEventListener("resize", () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// 启动动画
animate();